Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shield rotation #716

Merged
merged 29 commits into from
Mar 15, 2022
Merged

Shield rotation #716

merged 29 commits into from
Mar 15, 2022

Conversation

wipfli
Copy link
Member

@wipfli wipfli commented Dec 14, 2021

Image symbols placed along a line currently rotate with the line, similar to what text along a river or street does.
We would like to place multiple images along a line that are always upright.
The images are highway shields and highways in the US can have multiple shields.
This pull requests is work in progress and facilitates collaboration on the new feature.

## Launch Checklist

  • Confirm your changes do not include backports from Mapbox projects (unless with compliant license) - if you are not sure about this, please ask!
  • Briefly describe the changes in this PR.
  • Include before/after visuals or gifs if this PR includes visual changes.
  • Write tests for all new functionality.
  • Document any changes to public APIs.
  • Post benchmark scores.
  • Manually test the debug page.
  • Suggest a changelog category: bug/feature/docs/etc. or "skip changelog".
  • Add an entry inside this element for inclusion in the maplibre-gl-js changelog: <changelog></changelog>.

@wipfli
Copy link
Member Author

wipfli commented Dec 14, 2021

To get started, run npm run start-debug and open http://0.0.0.0:9966/debug/shield-rotation.html#10/39.8087/-86.0879 in your browser. You should see something like this:

image

It is good that the shields are grouped, but their orientation should be vertical.

@wipfli
Copy link
Member Author

wipfli commented Dec 14, 2021

The code is copy-pasted from https://github.com/ZeLonewolf/maplibre-shield-rotation-sample and was written by @ZeLonewolf

@github-actions
Copy link
Contributor

github-actions bot commented Dec 14, 2021

Bundle size report:

Size Change: +42 B
Total Size Before: 194 kB
Total Size After: 194 kB

Output file Before After Change
maplibre-gl.js 185 kB 185 kB +42 B
maplibre-gl.css 9.25 kB 9.25 kB 0 B
ℹ️ View Details
Source file Before After Change
src/render/draw_symbol.ts 2.47 kB 2.49 kB +26 B
node_modules/murmurhash-js/murmurhash2_gc.js 251 B 266 B +15 B
src/data/feature_position_map.ts 555 B 569 B +14 B
src/data/bucket/fill_attributes.ts 98 B 111 B +13 B
node_modules/gl-matrix/esm/vec3.js 825 B 837 B +12 B
src/render/painter.ts 3.56 kB 3.57 kB +12 B
src/util/is_char_in_unicode_block.ts 876 B 885 B +9 B
src/symbol/symbol_layout.ts 3.62 kB 3.63 kB +9 B
src/render/draw_hillshade.ts 1.09 kB 1.1 kB +9 B
src/style-spec/validate/validate.ts 378 B 386 B +8 B
src/symbol/projection.ts 1.78 kB 1.79 kB +8 B
src/render/program/collision_program.ts 716 B 724 B +8 B
src/util/debug.ts 156 B 164 B +8 B
src/util/evented.ts 3.9 kB 3.9 kB +7 B
src/style/evaluation_parameters.ts 386 B 393 B +7 B
src/style-spec/expression/definitions/index.ts 1.63 kB 1.63 kB +6 B
src/style/style_layer/hillshade_style_layer_properties.g.ts 155 B 161 B +6 B
src/style/query_utils.ts 259 B 264 B +5 B
node_modules/@mapbox/vector-tile/lib/vectortilefeature.js 1.01 kB 1.01 kB +5 B
src/symbol/anchor.ts 166 B 171 B +5 B
src/style/style_layer/symbol_style_layer.ts 1.02 kB 1.02 kB +5 B
src/style/load_glyph_range.ts 218 B 223 B +5 B
src/ui/map.ts 6.21 kB 6.22 kB +5 B
src/style-spec/validate/validate_glyphs_url.ts 168 B 172 B +4 B
node_modules/murmurhash-js/murmurhash3_gc.js 368 B 372 B +4 B
node_modules/gl-matrix/esm/mat4.js 2.05 kB 2.05 kB +4 B
node_modules/@mapbox/tiny-sdf/index.js 1.1 kB 1.11 kB +4 B
src/source/raster_dem_tile_source.ts 957 B 961 B +4 B
src/render/program/fill_program.ts 568 B 572 B +4 B
src/render/program/line_program.ts 1.15 kB 1.15 kB +4 B
src/render/draw_heatmap.ts 1.03 kB 1.04 kB +4 B
src/render/draw_custom.ts 334 B 338 B +4 B
src/ui/handler/handler_util.ts 105 B 109 B +4 B
src/style-spec/validate/validate_function.ts 1.14 kB 1.14 kB +3 B
src/style-spec/validate/validate_enum.ts 207 B 210 B +3 B
src/util/transferable_grid_index.ts 1.01 kB 1.02 kB +3 B
node_modules/gl-matrix/esm/vec4.js 372 B 375 B +3 B
src/util/color_ramp.ts 319 B 322 B +3 B
node_modules/@mapbox/vector-tile/index.js 90 B 93 B +3 B
src/data/bucket/fill_extrusion_bucket.ts 1.3 kB 1.3 kB +3 B
src/util/verticalize_punctuation.ts 584 B 587 B +3 B
src/shaders/_prelude.vertex.glsl.g.ts 473 B 476 B +3 B
src/shaders/symbol_text_and_icon.vertex.glsl.g.ts 962 B 965 B +3 B
src/render/program/circle_program.ts 451 B 454 B +3 B
src/ui/hash.ts 933 B 936 B +3 B
src/ui/handler/shim/drag_pan.ts 220 B 223 B +3 B
src/style-spec/validate/validate_paint_property.ts 52 B 54 B +2 B
src/style-spec/validate/validate_light.ts 289 B 291 B +2 B
src/data/segment.ts 449 B 451 B +2 B
src/data/program_configuration.ts 2.61 kB 2.61 kB +2 B
src/util/image.ts 676 B 678 B +2 B
src/util/classify_rings.ts 244 B 246 B +2 B
node_modules/@mapbox/vector-tile/lib/vectortilelayer.js 429 B 431 B +2 B
src/style/style_layer/fill_extrusion_style_layer_properties.g.ts 186 B 188 B +2 B
src/style/style_layer/line_style_layer.ts 959 B 961 B +2 B
src/style/parse_glyph_pbf.ts 364 B 366 B +2 B
src/symbol/quads.ts 1.88 kB 1.88 kB +2 B
src/symbol/collision_feature.ts 377 B 379 B +2 B
src/source/source.ts 355 B 357 B +2 B
src/source/source_state.ts 603 B 605 B +2 B
src/util/worker_pool.ts 418 B 420 B +2 B
src/shaders/collision_box.fragment.glsl.g.ts 146 B 148 B +2 B
src/shaders/fill_pattern.vertex.glsl.g.ts 387 B 389 B +2 B
src/geo/edge_insets.ts 430 B 432 B +2 B
src/ui/handler/tap_recognizer.ts 532 B 534 B +2 B
src/ui/handler/scroll_zoom.ts 1.25 kB 1.25 kB +2 B
src/ui/control/attribution_control.ts 1.09 kB 1.09 kB +2 B
src/ui/default_locale.ts 283 B 285 B +2 B
src/index.ts 827 B 829 B +2 B
src/style-spec/validate/validate_object.ts 391 B 392 B +1 B
src/style-spec/validate/validate_number.ts 220 B 221 B +1 B
src/style-spec/feature_filter/index.ts 892 B 893 B +1 B
src/style-spec/validate/validate_filter.ts 619 B 620 B +1 B
src/style-spec/validate/validate_string.ts 121 B 122 B +1 B
src/style/style_layer/circle_style_layer_properties.g.ts 226 B 227 B +1 B
src/data/bucket/heatmap_bucket.ts 81 B 82 B +1 B
src/style/style_layer/fill_style_layer_properties.g.ts 191 B 192 B +1 B
src/style/style_layer/line_style_layer_properties.g.ts 276 B 277 B +1 B
src/data/bucket/symbol_attributes.ts 766 B 767 B +1 B
src/symbol/get_anchors.ts 578 B 579 B +1 B
src/util/dispatcher.ts 331 B 332 B +1 B
src/symbol/collision_index.ts 1.61 kB 1.61 kB +1 B
src/style/style.ts 6.55 kB 6.56 kB +1 B
src/shaders/line_gradient.fragment.glsl.g.ts 350 B 351 B +1 B
node_modules/gl-matrix/esm/mat3.js 220 B 221 B +1 B
src/render/program/background_program.ts 473 B 474 B +1 B
src/gl/color_mode.ts 174 B 175 B +1 B
src/gl/stencil_mode.ts 151 B 152 B +1 B
src/render/draw_raster.ts 992 B 993 B +1 B
src/render/draw_debug.ts 1.1 kB 1.1 kB +1 B
src/ui/control/logo_control.ts 595 B 596 B +1 B
src/style-spec/validate/validate_source.ts 615 B 614 B -1 B
src/style-spec/validate/validate_color.ts 141 B 140 B -1 B
src/style-spec/validate_style.min.ts 284 B 283 B -1 B
src/style/validate_style.ts 153 B 152 B -1 B
src/util/script_detection.ts 1.65 kB 1.65 kB -1 B
src/source/rtl_text_plugin.ts 847 B 846 B -1 B
src/data/bucket/circle_attributes.ts 93 B 92 B -1 B
src/render/uniform_binding.ts 648 B 647 B -1 B
src/data/evaluation_feature.ts 96 B 95 B -1 B
src/data/bucket/line_attributes.ts 120 B 119 B -1 B
src/data/bucket/line_attributes_ext.ts 104 B 103 B -1 B
src/symbol/transform_text.ts 204 B 203 B -1 B
node_modules/potpack/index.mjs 353 B 352 B -1 B
src/symbol/symbol_size.ts 708 B 707 B -1 B
node_modules/tinyqueue/index.js 360 B 359 B -1 B
src/data/bucket/symbol_bucket.ts 4.26 kB 4.26 kB -1 B
src/style/style_layer/symbol_style_layer_properties.g.ts 654 B 653 B -1 B
src/style/light.ts 561 B 560 B -1 B
src/util/global_worker_pool.ts 315 B 314 B -1 B
src/shaders/_prelude.fragment.glsl.g.ts 120 B 119 B -1 B
src/shaders/background.fragment.glsl.g.ts 139 B 138 B -1 B
src/shaders/heatmap_texture.vertex.glsl.g.ts 144 B 143 B -1 B
src/shaders/symbol_text_and_icon.fragment.glsl.g.ts 597 B 596 B -1 B
src/render/draw_line.ts 1.01 kB 1.01 kB -1 B
src/util/throttle.ts 144 B 143 B -1 B
src/ui/handler_inertia.ts 820 B 819 B -1 B
src/ui/events.ts 357 B 356 B -1 B
src/ui/handler/shim/dblclick_zoom.ts 151 B 150 B -1 B
src/ui/handler/shim/drag_rotate.ts 179 B 178 B -1 B
src/ui/control/geolocate_control.ts 2.26 kB 2.26 kB -1 B
src/style-spec/expression/types.ts 509 B 507 B -2 B
src/style-spec/validate/validate_image.ts 62 B 60 B -2 B
src/util/web_worker_transfer.ts 952 B 950 B -2 B
src/util/struct_array.ts 698 B 696 B -2 B
src/util/intersection_tests.ts 942 B 940 B -2 B
src/style/style_layer/fill_extrusion_style_layer.ts 925 B 923 B -2 B
src/symbol/shaping.ts 3.62 kB 3.62 kB -2 B
src/symbol/check_max_angle.ts 289 B 287 B -2 B
src/source/tile_cache.ts 557 B 555 B -2 B
src/source/pixels_to_tile_units.ts 101 B 99 B -2 B
src/shaders/shaders.ts 1.37 kB 1.36 kB -2 B
src/shaders/fill_pattern.fragment.glsl.g.ts 396 B 394 B -2 B
src/shaders/line_gradient.vertex.glsl.g.ts 706 B 704 B -2 B
src/render/program/hillshade_program.ts 871 B 869 B -2 B
src/gl/vertex_buffer.ts 541 B 539 B -2 B
src/gl/context.ts 1.28 kB 1.28 kB -2 B
src/ui/handler/shim/touch_zoom_rotate.ts 291 B 289 B -2 B
src/style-spec/validate/validate_layer.ts 865 B 862 B -3 B
src/data/extent.ts 34 B 31 B -3 B
node_modules/gl-matrix/esm/common.js 174 B 171 B -3 B
src/data/bucket/pattern_bucket_features.ts 326 B 323 B -3 B
src/symbol/one_em.ts 32 B 29 B -3 B
src/util/web_worker.ts 42 B 39 B -3 B
src/render/program/debug_program.ts 198 B 195 B -3 B
src/gl/index_buffer.ts 300 B 297 B -3 B
src/render/draw_collision_debug.ts 1.08 kB 1.08 kB -3 B
src/ui/handler_manager.ts 2.33 kB 2.33 kB -3 B
src/style/zoom_history.ts 181 B 177 B -4 B
node_modules/@mapbox/vector-tile/lib/vectortile.js 187 B 183 B -4 B
src/data/bucket/line_bucket.ts 2.41 kB 2.41 kB -4 B
src/style-spec/deref.ts 212 B 208 B -4 B
src/render/program/fill_extrusion_program.ts 801 B 797 B -4 B
src/render/program/symbol_program.ts 1.3 kB 1.3 kB -4 B
src/render/draw_fill_extrusion.ts 804 B 800 B -4 B
src/render/draw_circle.ts 576 B 572 B -4 B
src/ui/handler/box_zoom.ts 717 B 713 B -4 B
src/util/task_queue.ts 269 B 265 B -4 B
src/render/image_manager.ts 1.4 kB 1.4 kB -5 B
src/ui/control/navigation_control.ts 1.61 kB 1.61 kB -5 B
src/style-spec/validate/validate_expression.ts 457 B 451 B -6 B
src/data/bucket/circle_bucket.ts 972 B 966 B -6 B
src/util/offscreen_canvas_supported.ts 158 B 152 B -6 B
src/render/draw_fill.ts 976 B 970 B -6 B
src/ui/camera.ts 2.98 kB 2.97 kB -6 B
src/style-spec/expression/index.ts 1.61 kB 1.6 kB -7 B
src/render/glyph_manager.ts 956 B 949 B -7 B
src/render/program/heatmap_program.ts 565 B 558 B -7 B
src/util/primitives.ts 978 B 971 B -7 B
src/style/properties.ts 1.87 kB 1.86 kB -8 B
src/render/program/program_uniforms.ts 827 B 818 B -9 B
node_modules/murmurhash-js/index.js 76 B 66 B -10 B
src/style/style_layer/heatmap_style_layer_properties.g.ts 141 B 131 B -10 B
node_modules/earcut/src/earcut.js 2.64 kB 2.63 kB -10 B
node_modules/pbf/index.js 2.82 kB 2.81 kB -10 B
src/data/bucket/fill_extrusion_attributes.ts 122 B 109 B -13 B
src/data/bucket/pattern_attributes.ts 139 B 109 B -30 B

@wipfli
Copy link
Member Author

wipfli commented Dec 14, 2021

If text-rotate is set, glyph quads will be rotated by the following code:

if (textRotate) {
const sin = Math.sin(textRotate),
cos = Math.cos(textRotate),
matrix = [cos, -sin, sin, cos];
tl._matMult(matrix);
tr._matMult(matrix);
bl._matMult(matrix);
br._matMult(matrix);
}

@wipfli
Copy link
Member Author

wipfli commented Dec 14, 2021

The glyph quads will rotate more later to align with the orientation of the line.

@wipfli
Copy link
Member Author

wipfli commented Dec 15, 2021

I think I found a way to achieve what we want:

image

For this what I changed was this:

highp float segment_angle = -a_projected_pos[2];

Which I hard-coded to zero, highp float segment_angle = 0.0.

Of course, this is not sustainable as it basically breaks the rest of the library but at least I found a line in the code-base where a tweak will stop symbols and text from being rotated.

@ZeLonewolf
Copy link
Contributor

That preview looks awesome! It would have taken me forever just to find the spot in the codebase where that code even happens.

@wipfli
Copy link
Member Author

wipfli commented Dec 15, 2021

Doing this in the shaders as suggested above is probably the worst option. We can achieve the same result by hard-coding something in a TypeScript function called placeGlyphAlongLine(), which seems already a bit better:

function placeGlyphAlongLine(offsetX: number,
lineOffsetX: number,
lineOffsetY: number,
flip: boolean,
anchorPoint: Point,
tileAnchorPoint: Point,
anchorSegment: number,
lineStartIndex: number,
lineEndIndex: number,
lineVertexArray: SymbolLineVertexArray,
labelPlaneMatrix: mat4,
projectionCache: {
[_: number]: Point;
}) {
const combinedOffsetX = flip ?
offsetX - lineOffsetX :
offsetX + lineOffsetX;
let dir = combinedOffsetX > 0 ? 1 : -1;
let angle = 0;
if (flip) {
// The label needs to be flipped to keep text upright.
// Iterate in the reverse direction.
dir *= -1;
angle = Math.PI;
}
if (dir < 0) angle += Math.PI;
let currentIndex = dir > 0 ?
lineStartIndex + anchorSegment :
lineStartIndex + anchorSegment + 1;
let current = anchorPoint;
let prev = anchorPoint;
let distanceToPrev = 0;
let currentSegmentDistance = 0;
const absOffsetX = Math.abs(combinedOffsetX);
const pathVertices = [];
while (distanceToPrev + currentSegmentDistance <= absOffsetX) {
currentIndex += dir;
// offset does not fit on the projected line
if (currentIndex < lineStartIndex || currentIndex >= lineEndIndex)
return null;
prev = current;
pathVertices.push(current);
current = projectionCache[currentIndex];
if (current === undefined) {
const currentVertex = new Point(lineVertexArray.getx(currentIndex), lineVertexArray.gety(currentIndex));
const projection = project(currentVertex, labelPlaneMatrix);
if (projection.signedDistanceFromCamera > 0) {
current = projectionCache[currentIndex] = projection.point;
} else {
// The vertex is behind the plane of the camera, so we can't project it
// Instead, we'll create a vertex along the line that's far enough to include the glyph
const previousLineVertexIndex = currentIndex - dir;
const previousTilePoint = distanceToPrev === 0 ?
tileAnchorPoint :
new Point(lineVertexArray.getx(previousLineVertexIndex), lineVertexArray.gety(previousLineVertexIndex));
// Don't cache because the new vertex might not be far enough out for future glyphs on the same segment
current = projectTruncatedLineSegment(previousTilePoint, currentVertex, prev, absOffsetX - distanceToPrev + 1, labelPlaneMatrix);
}
}
distanceToPrev += currentSegmentDistance;
currentSegmentDistance = prev.dist(current);
}
// The point is on the current segment. Interpolate to find it.
const segmentInterpolationT = (absOffsetX - distanceToPrev) / currentSegmentDistance;
const prevToCurrent = current.sub(prev);
const p = prevToCurrent.mult(segmentInterpolationT)._add(prev);
// offset the point from the line to text-offset and icon-offset
p._add(prevToCurrent._unit()._perp()._mult(lineOffsetY * dir));
const segmentAngle = angle + Math.atan2(current.y - prev.y, current.x - prev.x);
pathVertices.push(p);
return {
point: p,
angle: segmentAngle,
path: pathVertices
};
}

If I make the return statement this:

    return {
        point: p,
        angle: 0.0, // segmentAngle,
        path: pathVertices
    };

I also get upright shields.

@wipfli
Copy link
Member Author

wipfli commented Dec 15, 2021

image

This hack does not work with rotated or pitched maps. What would be your expectation if the map is rotated or pitched?

@ZeLonewolf
Copy link
Contributor

For a rotated or tilted map, I would expect the shields to tilt while maintaining a vertical orientation. What does ordinary text currently do on rotation? Does it maintain an upright bias like it does on an unrotated/untilted map?

@wipfli
Copy link
Member Author

wipfli commented Dec 16, 2021

'text-pitch-alignment': 'viewport' and the hard-coded zero angle in placeGlyphAlongLine() gives the following result:

image

This is probably quite close to what we want. It is just a bit strange that at zero pitch, i.e., default view which is perpendicular to the map, the shield looks quite bad (worse than without the text-pitch-alignment).

Without text-pitch-alignement (good):

image

With text-pitch-alignment without pitching the map (bad):

image

With text-pitch-alignment with pitching the map (good):

image

@wipfli
Copy link
Member Author

wipfli commented Dec 16, 2021

I added a new boolean style property called text-rotate-to-line. If true (default), the text rotates to the orientation of the line. If you set it to false, letters will not care about the rotation of the line they are attached to. This is the case we want to have for the highway shields.

@ZeLonewolf can you try it out and give me some feedback?

@ZeLonewolf
Copy link
Contributor

Thanks @wipfli I would be thrilled to test this out. I'll let you know how it goes.

@wipfli
Copy link
Member Author

wipfli commented Dec 16, 2021

text-writing-mode was introduced for solving possibly similar issues with CJK characters, see mapbox/mapbox-gl-js#8399. My understanding so far is that it is limited to 'symbol-placement': 'point'.

@ZeLonewolf
Copy link
Contributor

Wow! This looks awesome in 2D in Indiana:

image
image
image

Pan and tilt:

image

We might want to change the orientation on the pan and tilt version, but this is already better than the status quo.

@ZeLonewolf
Copy link
Contributor

One thing I've noticed is that the corners of icons overlap when they're placed along a diagonal, but there's space between them when they're arranged vertically or horizontally. In the screenshots below, there is a single space character between each icon pair:

image
image

@wipfli
Copy link
Member Author

wipfli commented Dec 28, 2021

I wonder if MapLibre handles this correctly for collisions that have nothing to do with my change. Can you check what happens if you set text-rotate to 45 degrees, do the corners overlap on a horizontal line? I noticed that collision logic is handled in a separate file from the placement and rendering code. Maybe I forgot something there...

@wipfli
Copy link
Member Author

wipfli commented Dec 28, 2021

By the way I love all these little shields with their strange shapes. Some of them look like a map on the map.

@ZeLonewolf
Copy link
Contributor

With text-rotate: 45, on a vertical stack I do see overlap:
image

On a 45-degree road, I get the expected spacing:
image

@ZeLonewolf
Copy link
Contributor

@wipfli can you merge from main? I'm attempting to build this branch using npm, which is currently broken and fixed by #704.

@wipfli
Copy link
Member Author

wipfli commented Jan 3, 2022

If you want to build the library from source code and produce the files in the dist folder yourself, you can run:

nvm use 14
npm ci
npm run build-prod
ls dist
# maplibre-gl.css  maplibre-gl-dev.js  maplibre-gl.d.ts  maplibre-gl-unminified.js  maplibre-gl-unminified.js.map  package.json

@wipfli
Copy link
Member Author

wipfli commented Jan 3, 2022

Since text-rotate: 45 also results in overlapping shields, I think that my new feature behaves the same wrong way as the existing code does when it comes to rotated shields.

@ZeLonewolf
Copy link
Contributor

Since text-rotate: 45 also results in overlapping shields, I think that my new feature behaves the same wrong way as the existing code does when it comes to rotated shields.

I agree. This is an issue for letters also, which overlap when following lines with too tight of a radius.

@msbarry
Copy link
Contributor

msbarry commented Jan 13, 2022

Hey - quick question here - for onthegomap.com, I implemented highway shields by using:

  'layout': {
    'icon-image': [
      'concat',
      ['get', 'network'],
      '-',
      ['to-string', ['max', 2, ['get', 'ref_length']]],
    ],
    'icon-rotation-alignment': 'viewport',
    'symbol-placement': 'line',
    'symbol-spacing': 500,
    'text-field': ['to-string', ['get', 'ref']],
    'text-font': ['Robonoto Regular'],
    'text-offset': [0, 0.1],
    'text-rotation-alignment': 'viewport',
    'text-size': 10,
    'icon-size': 0.9,
  },

2 questions:

  1. Would 'text-rotation-alignment': 'viewport' and 'icon-rotation-alignment': 'viewport' do what you're trying to achieve @ZeLonewolf?
  2. What is the benefit of dynamically constructing the image with text over using the approach of a text symbol and icon symbol like I'm using?

Also, I like the highway shield images! Are they available anywhere for other sites to use?

@HarelM
Copy link
Collaborator

HarelM commented Mar 10, 2022

This is my MO basically:

  1. Start writing a unit test.
  2. Mock the hell out of everything until I get to the relevant part of the code which I'm interested in testing.
  3. Copy stuff from other tests to pass at part of the step above.
  4. Make the test pass.

Here's my initial code, far from complete at it fails somewhere in the middle of the code.
Unfortunately, I don't have more time tonight to continue working on it.
draw_symbol.test.ts:

import {SymbolLayerSpecification} from '../style-spec/types.g';
import EvaluationParameters from '../style/evaluation_parameters';
import SymbolStyleLayer from '../style/style_layer/symbol_style_layer';
import ZoomHistory from '../style/zoom_history';
import drawSymbol from './draw_symbol';
import Painter from './painter';
jest.mock('./painter');

describe('drawSymbol', () => {
    test('should not do anything', () => {
        const mockPainter = new Painter(null, null);
        mockPainter.renderPass = 'opaque';

        drawSymbol(mockPainter, null, null, null, null);

        expect(mockPainter.colorModeForRenderPass).not.toHaveBeenCalled();
    });

    test('should draw and rotate glyphs according to the line', () => {
        const mockPainter = new Painter(null, null);
        mockPainter.context = {
            gl: jest.fn() as any
        } as any;
        mockPainter.renderPass = 'translucent';
        const layerSpec = {
            id: 'mock-layer',
            source: 'empty-source',
            type: 'symbol',
            layout: {
                'text-variable-anchor': ['center']
            },
            paint: {}
        } as SymbolLayerSpecification;
        const layer = new SymbolStyleLayer(layerSpec);
        layer.recalculate({zoom: 0, zoomHistory: {} as ZoomHistory} as EvaluationParameters, []);
        // a lot more mocking is needed here, I copy it from different tests until it passes the line of code I'm not interested in, in the production code
        drawSymbol(mockPainter, null, layer, [], null);

        // expect program.draw to be called with specific parameters
    });
});

@HarelM
Copy link
Collaborator

HarelM commented Mar 11, 2022

The following is the entire test file that gets to the program.draw method:

import { mat4 } from 'gl-matrix';
import SymbolBucket from '../data/bucket/symbol_bucket';
import SourceCache from '../source/source_cache';
import Tile from '../source/tile';
import {OverscaledTileID} from '../source/tile_id';
import {SymbolLayerSpecification} from '../style-spec/types.g';
import EvaluationParameters from '../style/evaluation_parameters';
import SymbolStyleLayer from '../style/style_layer/symbol_style_layer';
import ZoomHistory from '../style/zoom_history';
import drawSymbol from './draw_symbol';
import Painter from './painter';
import Program from './program';
jest.mock('./painter');
jest.mock('./program');
jest.mock('../source/source_cache');
jest.mock('../source/tile');
jest.mock('../data/bucket/symbol_bucket');

describe('drawSymbol', () => {
    test('should not do anything', () => {
        const mockPainter = new Painter(null, null);
        mockPainter.renderPass = 'opaque';

        drawSymbol(mockPainter, null, null, null, null);

        expect(mockPainter.colorModeForRenderPass).not.toHaveBeenCalled();
    });

    test('should update variable anchors', () => {

        const mockPainter = new Painter(null, null);
        mockPainter.context = {
            gl: {},
            activeTexture: {
                set: () => {}
            }
        } as any;
        mockPainter.renderPass = 'translucent';
        mockPainter.transform = { pitch: 0, labelPlaneMatrix: mat4.create() } as any;
        mockPainter.options = {} as any;

        const layerSpec = {
            id: 'mock-layer',
            source: 'empty-source',
            type: 'symbol',
            layout: {
                //'text-variable-anchor': ['center']
            },
            paint: {
                'text-opacity': 1
            }
        } as SymbolLayerSpecification;
        const layer = new SymbolStyleLayer(layerSpec);
        layer.recalculate({zoom: 0, zoomHistory: {} as ZoomHistory} as EvaluationParameters, []);
        
        const tileId = new OverscaledTileID(1, 0, 1, 0, 0);
        tileId.posMatrix = mat4.create();
        const programMock = new Program(null, null, null, null, null, null);
        (mockPainter.useProgram as jest.Mock).mockReturnValue(programMock);
        const bucket = new SymbolBucket(null);
        bucket.icon = {
            programConfigurations: {
                get: () => {}
            },
            segments: {
                get: () => [1]
            }
        } as any;
        bucket.iconSizeData = {
            kind: 'constant',
            layoutSize: 1
        }
        const tile = new Tile(tileId, 256);
        tile.tileID = tileId;
        tile.imageAtlasTexture = {
            bind: () => {}
        } as any;
        (tile.getBucket as jest.Mock).mockReturnValue(bucket);
        const sourceCacheMock = new SourceCache(null, null, null);
        (sourceCacheMock.getTile as jest.Mock).mockReturnValue(tile);
        sourceCacheMock.map = { showCollisionBoxes: false } as any;

        drawSymbol(mockPainter, sourceCacheMock, layer, [tileId], null);

        expect(programMock.draw).toHaveBeenCalledTimes(1);
    });
});

I have added a lot of typings to the draw_symbol.ts in order to better understand how it works.
I'll create a PR for this as it is not related to this issue but a general improvement.
I need to see if it collides with the terrain 3d and fix the relevant conflicts.
PR will be sent shortly.

@HarelM
Copy link
Collaborator

HarelM commented Mar 11, 2022

This should conflict, shouldn't it?

@wipfli
Copy link
Member Author

wipfli commented Mar 11, 2022

Merged main into shield-rotation without conflicts. Where did you expect a collision?

@HarelM
Copy link
Collaborator

HarelM commented Mar 11, 2022

In the draw symbol file. But I might have underestimated the merge engine :-)

@HarelM
Copy link
Collaborator

HarelM commented Mar 11, 2022

Are you now able to write the relevant test(s) or is my assistance still needed?

@wipfli
Copy link
Member Author

wipfli commented Mar 11, 2022

Thanks for adding that test @HarelM. Sorry if I am a bit slow to understand here, but which part of draw_symbol.ts should we cover with unit testing? Currently not coverd in draw_symbol.ts are the lines

70,114-235,240,242,306-313,334,346-351,374-377,393,402-404,409-414

@HarelM
Copy link
Collaborator

HarelM commented Mar 11, 2022

I didn't cover everything in that PR. I suggest only to add a test for this logic that was changed as part of this PR.

@HarelM
Copy link
Collaborator

HarelM commented Mar 11, 2022

In general, we should cover as much as possible. But I don't want to hold this PR due to low coverage.

@HarelM
Copy link
Collaborator

HarelM commented Mar 11, 2022

Overall very nice!
There's a need to take into consideration the auto possibility and make sure it is working as expected with all the ifs.
Other than that, it looks good!

@HarelM
Copy link
Collaborator

HarelM commented Mar 12, 2022

I think this PR should wait until #1022 will be merges since I think solving conflicts in this PR will be easier...

@wipfli
Copy link
Member Author

wipfli commented Mar 14, 2022

Thanks for the auto hint. I added a render test to text-pitch-alignment: auto since this is what will be affected. I am fine with waiting until terrain3d is merged!

@HarelM
Copy link
Collaborator

HarelM commented Mar 14, 2022

Cool, I'll approve, you can decide if you want to merge this into main and then merge to terrain3d or wait, your call.

@wipfli
Copy link
Member Author

wipfli commented Mar 15, 2022

Thanks for reviewing @HarelM. I will merge this to main and then merge main into #1022.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants